package com.yumeng.framework.auth.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.*;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.Verification;
import com.yumeng.common.api.ApiCode;
import com.yumeng.common.data.constants.LogConstants;
import com.yumeng.common.helper.AssertHelper;
import com.yumeng.common.helper.BaseEnumHelper;
import com.yumeng.common.utils.DigestUtils;
import com.yumeng.common.utils.UUIDUtils;
import com.yumeng.common.auth.BaseAuthHelper;
import com.yumeng.framework.auth.bean.LoginTokenInfo;
import com.yumeng.framework.auth.bean.TokenInfo;
import com.yumeng.common.auth.BaseAuthInfo;
import com.yumeng.framework.auth.bean.authinfo.NormalAuthInfo;
import com.yumeng.common.data.enums.AuthClientType;
import com.yumeng.framework.auth.data.enums.JwtType;
import com.yumeng.framework.auth.exception.TokenAuthException;
import com.yumeng.config.helper.PropHelper;
import com.yumeng.config.properties.YumengAuthProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;

import java.util.Date;
import java.util.Map;

/**
 * @author wxd
 * @date 2022/4/14 21:28
 */
@Slf4j(topic = LogConstants.TOKEN_FILE)
public class JwtUtils {

    private static final String TOKEN_NAME_AUTH_KEY = "aaa";//认证Key 类似用户名username
    private static final String TOKEN_NAME_IDENTITY_ID = "bbb";//身份id 类似userId
    private static final String TOKEN_NAME_JWT_TYPE = "ccc";//jwtType token的类型，区分是用于pc、mobile、远程对接等
    private static final String TOKEN_NAME_LOGIN_TIME = "ddd";//登录时间
    private static final String TOKEN_NAME_ENCRYPT_REFRESH_TOKEN = "eee";//refreshToken加密值，防止refreshToken泄露

    private static YumengAuthProperties.JwtProperties getJwtProperties(){
        return PropHelper.getJwtProperties();
    }

    /**
     * 生成JWT Token（用于登录）
     * 使用配置文件中的超时时间
     * @param authInfo
     * @return
     */
    public static LoginTokenInfo generateTokenForLogin(BaseAuthInfo authInfo, JwtType jwtType, Map<String, Object> loginInfo){
        String newRefreshToken = null;
        boolean needRefreshToken = needRefreshToken(jwtType);
        if (needRefreshToken){
            newRefreshToken = UUIDUtils.getUuidNoFlag();
        }
        String newJwt = generateToken(authInfo, jwtType, newRefreshToken, null, needRefreshToken);
        return new LoginTokenInfo(newJwt, newRefreshToken, loginInfo);
    }

    /**
     * 刷新token
     * 流程完善后，去掉@Deprecated
     * @param newAuthInfo 重新获取的（一般是从数据库中获取最新信息）
     * @param oldTokenInfo
     * @return
     */
    @Deprecated
    public static TokenInfo refreshToken(BaseAuthInfo newAuthInfo, TokenInfo oldTokenInfo){
        if (!refreshTokenEnable()){
            throw new TokenAuthException("请开启刷新token配置");
        }
        //如果需要支持其他认证类型，需实现对应的equals方法
        if (!(newAuthInfo instanceof NormalAuthInfo)){
            throw new TokenAuthException("当前认证类型不支持刷新令牌：" + newAuthInfo.getClass().getSimpleName());
        }
        String oldToken = oldTokenInfo.getToken();
        String oldRefreshToken = oldTokenInfo.getRefreshToken();
        AssertHelper.assertNotBlank(oldToken, "刷新jwtToken oldToken is blank");
        AssertHelper.assertNotBlank(oldRefreshToken, "刷新jwtToken oldRefreshToken is blank");
        DecodedJWT decodedJWT = getDecodedJWT(oldToken);
        JwtType jwtType = getJwtType(decodedJWT);
        boolean needRefreshToken = needRefreshToken(jwtType);
        if (!needRefreshToken){
            return null;
        }
        if (isExpired(decodedJWT)){//如果jwt已超时
            checkRefreshCode(decodedJWT, oldRefreshToken);
        }else{
            return null;
        }
        //TODO  1、doCheck是否需要  2、比较authInfo是否发生变化   注意跟踪equals方法 BaseAuthHelper.getCurrAuthInfo()和缓存中的其实是一样的？
        BaseAuthInfo oldAuthInfo = BaseAuthHelper.getCurrAuthInfo();
        if (!oldAuthInfo.equals(newAuthInfo)){
            throw new TokenAuthException("认证信息已变更，请重新登录").setApiCode(ApiCode.UNAUTHORIZED);
        }
        Date loginTime = getLoginTime(decodedJWT);
        AssertHelper.assertNotNull(loginTime, "刷新jwtToken loginTime is null");
        String newRefreshToken = UUIDUtils.getUuidNoFlag();
        String newJwt = generateToken(newAuthInfo, jwtType, newRefreshToken, loginTime, needRefreshToken);
        //刷新token前会比较当前认证信息 authInfo 信息是否有变化，如果已改变，会强制要求重新登录，如果一致，才允许刷新，因此也不需要返回info信息了（能刷新，说明authInfo未变）
        return new TokenInfo(newJwt, newRefreshToken);
    }

    /**
     * 验证token
     * @param token
     * @param salt
     * @param acceptExpiresAt 虽然token已过期，但某些场景可以通过设置该值临时放行（提升用户体验），比如当前携带了有效的refreshToken
     * @return
     */
    public static boolean verifyToken(String token, String salt, String authKey, String identityId, Long acceptExpiresAt) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(salt);
            Verification verification = JWT.require(algorithm)
                    // 签发人
                    .withIssuer(getJwtProperties().getIssuer())
                    // 主题
                    .withSubject(getJwtProperties().getSubject())
                    // 签发的目标
                    .withAudience(getJwtProperties().getAudience())
                    .withClaim(TOKEN_NAME_AUTH_KEY, authKey)
                    .withClaim(TOKEN_NAME_IDENTITY_ID, identityId);
            if (acceptExpiresAt != null){
                verification = verification.acceptExpiresAt(acceptExpiresAt);//当token超时，用此参数来作为缓冲验证
            }
            JWTVerifier verifier = verification.build();
            DecodedJWT jwt = verifier.verify(token);
            if (jwt != null) {
                return true;
            }
        } catch (AlgorithmMismatchException e) {
            log.error("令牌算法不匹配", e);
        }catch (SignatureVerificationException e) {
            log.error("签名无效", e);
        }catch (TokenExpiredException e) {
            log.error("令牌已过期", e);
            if (acceptExpiresAt == null){
                //acceptExpiresAt 为null 说明是初次验证，抛出上层，传入acceptExpiresAt再次验证
                throw e;
            }
        }catch (InvalidClaimException e) {
            log.error("Claim值无效", e);
        }catch (JWTVerificationException e) {
            log.error("令牌验证异常", e);
        }
        return false;
    }

    public static void checkRefreshCode(String jwt, String targetRefreshToken){
        DecodedJWT decodedJWT = getDecodedJWT(jwt);
        checkRefreshCode(decodedJWT, targetRefreshToken);
    }
    /**
     * 判断refreshToken 与 jwtToken是否匹配
     * @return
     */
    public static void checkRefreshCode(DecodedJWT decodedJWT, String targetRefreshToken){
        if (decodedJWT == null || StringUtils.isBlank(targetRefreshToken)){
            throw new IllegalArgumentException("checkRefreshCode 参数缺失");
        }
        //验证refreshToken与jwt是否匹配
        String jwtId = getJwtId(decodedJWT);
        String sourceEncryptRefreshToken = getEncryptRefreshToken(decodedJWT);
        String targetEncryptRefreshToken = encryptRefreshToken(jwtId, targetRefreshToken);
        if (!sourceEncryptRefreshToken.equals(targetEncryptRefreshToken)){
            throw new TokenAuthException("refreshToken异常");
        }
        //验证refreshToken是否超时
        int expireSeconds = getRefreshTokenExpireSeconds(getJwtType(decodedJWT));
        Date issuedDate = getIssuedDate(decodedJWT);
        Date expireDate = DateUtils.addSeconds(issuedDate, expireSeconds);
        if (expireDate.before(new Date())){
            throw new TokenAuthException("refreshToken已过期").setApiCode(ApiCode.AUTH_EXPIRED);
        }
    }

    /**
     * 生成JWT Token
     * @param authInfo
     * @param jwtType jwt类型
     * @param needRefreshToken 是否需要refreshToken流程
     * @return token
     */
    private static String generateToken(BaseAuthInfo authInfo, JwtType jwtType, String refreshToken, Date loginTime, boolean needRefreshToken) {
        try {
            AssertHelper.assertNotNull(authInfo.getIdentityId(), "identityId不能为空");
            log.debug("identityId:{}", authInfo.getIdentityId());
            AssertHelper.assertNotBlank(authInfo.getAuthKey(), "authKey不能为空");
            log.debug("authKey:{}", authInfo.getAuthKey());
            // 如果盐值为空，则使用默认值
            String salt = authInfo.getSalt();
            if (StringUtils.isBlank(salt)) {
                //salt = getJwtProperties().getSecret();
                //强制使用authInfo中的salt，安全性更高
                throw new TokenAuthException("认证信息salt缺失");
            }
            log.debug("salt:{}", salt);

            Date issuedDate = new Date();
            // 过期时间，单位：秒
            Long expireSeconds = getTokenExpireSeconds(jwtType);
            log.debug("expireSeconds:{}", expireSeconds);
            Date expireDate = DateUtils.addSeconds(issuedDate, expireSeconds.intValue());
            log.debug("expireDate:{}", expireDate);
            //登录时间
            loginTime = loginTime != null ? loginTime : issuedDate;
            log.debug("loginTime:{}", loginTime);

            //加密后的RefreshToken
            String jwtId = UUIDUtils.getUuidNoFlag();
            String encryptRefreshToken = null;
            if (needRefreshToken){
                AssertHelper.assertNotBlank(refreshToken, "refreshToken不能为空");
                encryptRefreshToken = encryptRefreshToken(jwtId, refreshToken);
            }

            // 生成token
            Algorithm algorithm = Algorithm.HMAC256(salt);
            String token = JWT.create()
                    .withClaim(TOKEN_NAME_IDENTITY_ID, authInfo.getIdentityId())
                    .withClaim(TOKEN_NAME_AUTH_KEY, authInfo.getAuthKey())
                    .withClaim(TOKEN_NAME_JWT_TYPE, jwtType.getCode())
                    .withClaim(TOKEN_NAME_LOGIN_TIME, loginTime)
                    .withClaim(TOKEN_NAME_ENCRYPT_REFRESH_TOKEN, encryptRefreshToken)
                    .withJWTId(jwtId)// jwt唯一id
                    .withIssuer(getJwtProperties().getIssuer())// 签发人
                    .withSubject(getJwtProperties().getSubject())// 主题
                    .withAudience(getJwtProperties().getAudience())// 签发的目标
                    .withIssuedAt(issuedDate)// 签名时间
                    .withExpiresAt(expireDate)// token过期时间
                    .sign(algorithm);// 签名
            log.info("生成jwt成功，id:{}，jwtType：{}，token：{}",jwtId, jwtType, token);
            return token;
        } catch (Exception e) {
            throw new TokenAuthException("生成jwt失败：%s", e.getMessage());
        }
    }

    //region 辅助

    /**
     * 获取过期时间秒数（token）
     * @param jwtType
     * @return
     */
    private static Long getTokenExpireSeconds(JwtType jwtType){
        if (JwtType.UNKNOWN.equals(jwtType) || JwtType.PC.equals(jwtType) || JwtType.MOBILE.equals(jwtType)){
            return getJwtProperties().getExpireSeconds();//默认2小时
        }else if (JwtType.REMOTE.equals(jwtType)){
            return getJwtProperties().getExpireSecondsRemote();//默认24小时
        }else{
            throw new TokenAuthException("生成jwt JwtType异常");
        }
    }

    /**
     * 是否需要执行RefreshToken流程
     * 目前仅PC端和mobile端需要执行refreshToken流程
     * @param jwtType
     * @return
     */
    public static boolean needRefreshToken(JwtType jwtType){
        if (JwtType.PC.equals(jwtType) || JwtType.MOBILE.equals(jwtType)){
            return true;
        }else if (JwtType.REMOTE.equals(jwtType) || JwtType.UNKNOWN.equals(jwtType)){
            return false;
        }else{
            throw new TokenAuthException("判断needRefreshToken JwtType异常");
        }
    }

    /**
     * 是否启用刷新token机制
     *
     * @return
     */
    public static boolean refreshTokenEnable(){
        return getJwtProperties().getRefreshToken();
    }

    /**
     * 获取过期时间秒数（刷新token）
     *
     * @param jwtType
     * @return
     */
    private static int getRefreshTokenExpireSeconds(JwtType jwtType){
        if (JwtType.PC.equals(jwtType)){
            return getJwtProperties().getRefreshTokenExpireSecondsPc();
        }else if (JwtType.MOBILE.equals(jwtType)){
            return getJwtProperties().getRefreshTokenExpireSecondsMobile();
        }else{
            throw new TokenAuthException("仅PC端和Mobile端支持refreshToken");
        }
    }

    /**
     * 加密 RefreshToken
     * @param jwtId
     * @param targetRefreshToken
     * @return
     */
    private static String encryptRefreshToken(String jwtId, String targetRefreshToken){
        return DigestUtils.md5Hex(jwtId.substring(3, 17) + targetRefreshToken);
    }

    /**
     * 解析token，获取token数据
     * @param token
     * @return
     */
    public static DecodedJWT getDecodedJWT(String token) {
        AssertHelper.assertNotBlank(token, "token is blank");
        return JWT.decode(token);
    }

    /**
     * 获取jwt的过期缓冲窗口
     * 当token过期时，如果当前携带了refreshToken，可
     * @param jwt
     * @return
     */
    public static long getLeewayForExpiresAt(String jwt){
        DecodedJWT decodedJwt = getDecodedJWT(jwt);
        JwtType jwtType = getJwtType(decodedJwt);
        long leeway = 0;
        if (JwtType.PC.equals(jwtType)){
            leeway = getJwtProperties().getRefreshTokenExpireSecondsPc();
        }else if (JwtType.MOBILE.equals(jwtType)){
            leeway = getJwtProperties().getRefreshTokenExpireSecondsMobile();
        }else{
            throw new TokenAuthException("仅PC端和Mobile端支持token过期窗口缓冲");
        }
        return leeway;
    }

    /**
     * 根据登录设备类型获取jwt类型
     *
     * @param authClientType
     * @return
     */
    public static JwtType getJwtTypeByAuthClientType(AuthClientType authClientType){
        if (AuthClientType.PC.equals(authClientType)){
            return JwtType.PC;
        }else if (AuthClientType.ANDROID.equals(authClientType) || AuthClientType.IOS.equals(authClientType)){
            return JwtType.MOBILE;
        }else if (AuthClientType.UNKNOWN.equals(authClientType)) {
            return JwtType.UNKNOWN;
        }else {
            throw new TokenAuthException("当前认证客户端类型未识别：%s", authClientType);
        }
    }

    //endregion

    //region 属性

    /**
     * 获取认证Name（用户名）
     * @param token
     * @return
     */
    public static String getAuthKey(String token) {
        DecodedJWT decodedJwt = getDecodedJWT(token);
        return getAuthKey(decodedJwt);
    }
    public static String getAuthKey(DecodedJWT decodedJwt) {
        return decodedJwt.getClaim(TOKEN_NAME_AUTH_KEY).asString();
    }

    /**
     * 获取身份标识（用户id）
     * @param token
     * @return
     */
    public static String getIdentityId(String token) {
        DecodedJWT decodedJwt = getDecodedJWT(token);
        return getIdentityId(decodedJwt);
    }
    public static String getIdentityId(DecodedJWT decodedJwt) {
        return decodedJwt.getClaim(TOKEN_NAME_IDENTITY_ID).asString();
    }


    /**
     * 获取jwt类型
     * @param token
     * @return
     */
    public static JwtType getJwtType(String token) {
        DecodedJWT decodedJwt = getDecodedJWT(token);
        return getJwtType(decodedJwt);
    }
    public static JwtType getJwtType(DecodedJWT decodedJwt) {
        Integer type = decodedJwt.getClaim(TOKEN_NAME_JWT_TYPE).asInt();
        return BaseEnumHelper.getEnum(JwtType.class, type);
    }

    /**
     * 获取创建时间
     * @param token
     * @return
     */
    public static Date getIssuedDate(String token) {
        DecodedJWT decodedJwt = getDecodedJWT(token);
        return getIssuedDate(decodedJwt);
    }
    public static Date getIssuedDate(DecodedJWT decodedJwt) {
        return decodedJwt.getIssuedAt();
    }

    /**
     * 获取过期时间
     * @param token
     * @return
     */
    public static Date getExpireDate(String token) {
        DecodedJWT decodedJwt = getDecodedJWT(token);
        return getExpireDate(decodedJwt);
    }
    public static Date getExpireDate(DecodedJWT decodedJwt) {
        return decodedJwt.getExpiresAt();
    }

    /**
     * 获取登录时间
     * @param token
     * @return
     */
    public static Date getLoginTime(String token){
        DecodedJWT decodedJwt = getDecodedJWT(token);
        return getLoginTime(decodedJwt);
    }
    public static Date getLoginTime(DecodedJWT decodedJwt){
        return decodedJwt.getClaim(TOKEN_NAME_LOGIN_TIME).asDate();
    }

    /**
     * 获取加密后的refreshToken
     * @param token
     * @return
     */
    public static String getEncryptRefreshToken(String token){
        DecodedJWT decodedJwt = getDecodedJWT(token);
        return getEncryptRefreshToken(decodedJwt);
    }
    public static String getEncryptRefreshToken(DecodedJWT decodedJwt){
        return decodedJwt.getClaim(TOKEN_NAME_ENCRYPT_REFRESH_TOKEN).asString();
    }

    /**
     * 判断token是否已过期
     * @param token
     * @return
     */
    public static boolean isExpired(String token) {
        DecodedJWT decodedJwt = getDecodedJWT(token);
        return isExpired(decodedJwt);
    }
    public static boolean isExpired(DecodedJWT decodedJwt){
        Date expireDate = getExpireDate(decodedJwt);
        if (expireDate == null) {
            return true;
        }
        return expireDate.before(new Date());
    }

    /**
     * 获取jwtId
     * @param token
     * @return
     */
    public static String getJwtId(String token){
        DecodedJWT decodedJwt = getDecodedJWT(token);
        return getJwtId(decodedJwt);
    }
    public static String getJwtId(DecodedJWT decodedJwt){
        return decodedJwt.getId();
    }

    //endregion


}
